/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package Server;

import Common.Game;
import Common.IBackupServer;
import Common.IClient;
import Common.IGameServer;
import Common.ImNotMasterServerException;
import Common.MessageType;
import Common.PlayernameAlreadyAssigned;
import java.io.Serializable;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import spread.AdvancedMessageListener;
import spread.MembershipInfo;
import spread.SpreadConnection;
import spread.SpreadException;
import spread.SpreadGroup;
import spread.SpreadMessage;

/**
 *
 * @author Mario,Thomas,Max,Martin,Sergio
 */
public class GameServerImpl extends UnicastRemoteObject implements IBackupServer, IGameServer, AdvancedMessageListener
{

    private String _spreadGroupName;
    private SpreadConnection _connection = null;
    private String _instanceName;
    private String _hostName;
    private Integer _port;
    private Boolean _isMaster = false;
    private IGameServer _currentMaster = null;
    private Serializable _skeleton = null;
    private Game _game = null;

    // Implementations must have an
    // explicit constructor
    // in order to declare the
    // RemoteException exception
    public GameServerImpl(String hostName, int port, String spreadPrivateName) throws RemoteException, UnknownHostException
    {
        super();

        _skeleton = (Serializable) GameServerImpl.toStub(this);
        this.StartNewGame();  //Game initialisieren

        this.ConnectToSpread(hostName, port, spreadPrivateName);
        //this.StartCheckSpreadLoop();
    }

    /**
     * Stellt die Verbindung zum SpreadDeamon her.
     * @param hostName
     * @param port
     * @param spreadPrivateName
     */
    private Boolean ConnectToSpread(String hostName, int port, String spreadPrivateName)
    {
        SpreadConnection connection = new SpreadConnection();

        try
        {
            try
            {
                connection.connect(Inet4Address.getByName(hostName), port, spreadPrivateName, false, true); //secondArg:0 heißt DefaultPort, firstARg:Wenn hostName == empty dann localhost, fourthArg: darf nur 10 Zeichen haben, lastArg: true, damit wir MembershipMessages bekommen
            }
            catch (UnknownHostException ex)
            {
                Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
                return false;
            }
            _instanceName = spreadPrivateName;
            _hostName = hostName;
            _port = port;
        }
        catch (SpreadException ex)
        {
            if (ex.toString().contains("Connection attempt rejected=-6"))
            {
                Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, "Es gibt bereits ein SpreadMitglied mit diesen Namen.");
                System.out.println("STOP:Es gibt bereits ein SpreadMitglied mit diesen Namen.");
            }

            Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }
        _connection = connection;   //Hold reference of the connection
        _connection.add(this);  //Register the AdvancedMessageListener
        return true;
    }

    /**
     * With this Method joins the Server the overgiven SPREAD Group
     * @param groupName
     * @return
     * @throws IllegalArgumentException
     */
    public Boolean JoinGroup(String groupName) throws IllegalArgumentException
    {
        if (groupName.isEmpty())
        {
            throw new IllegalArgumentException(groupName);
        }
        else
        {
            _spreadGroupName = groupName;
        }

        Boolean rval = false;
        SpreadGroup group = new SpreadGroup();
        try
        {
            group.join(_connection, groupName);

            rval = true;
        }
        catch (SpreadException ex)
        {
            Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
        }

        return rval;
    }

    /**
     * Sets the State of the Server to Idle and then the Server do his background work.
     */
    public void Idle()
    {
        while (true)
        {
            try
            {
                Thread.sleep(400);
            }
            catch (InterruptedException ex)
            {
                Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    /*
     * Erzwingt die Synchronisation der Server über Spread mit der Liste von Clients für das aktuelle Spiel
     * */
    public void forceSync(Vector<IClient> data)
    {
        if (this._connection != null)
        {
            try
            {
                SpreadMessage mes = new SpreadMessage();
                mes.setObject(data);
                mes.addGroup(_spreadGroupName);
                mes.setReliable();
                mes.setSelfDiscard(true);
                mes.setType(MessageType.ClientSync.type());
                this._connection.multicast(mes);
            }
            catch (SpreadException ex)
            {
                Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }


    /**
     * Methode wird verwendet um der Gruppe(inklusive) mir selbst mitzuteilen, dass eine Election stattfindet,
     * und dieser Server sie macht(d.h: er wird der nächste).
     */
    private void electionOfNewMaster()
    {
        if (this._connection != null)
        {
            try
            {
                SpreadMessage mes = new SpreadMessage();
                mes.addGroup(this._spreadGroupName);
                mes.setObject(this._skeleton);
                mes.setType(MessageType.ElectionMaster.type());
                mes.setCausal();
                this._connection.multicast(mes);
            }
            catch (SpreadException ex)
            {
                Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    /**
     * Ändert den Status des Servers entsprechend des Übergebenen Wertes.
     * @param value
     */
    private void SwitchToMasterMode(Object obj)
    {
        if (obj != null)
        {
            this._currentMaster = (IGameServer) obj;
            this._isMaster = this._skeleton.equals(obj);
            if (this._isMaster)
            {
                //Jetzt teilen wir noch jeden angemeldeten Client mit, dass es einen neuen MasterServer gibt
                //Machen wir nur für Unregister, Folgender Fall: Registriert auf Server-->Server schmiert ab, alle anderen eingetragenen
                //Server des Clients sind auch nicht erreichbar-->somit hat der Client keine Chance sich wieder zu deregistrieren-->deswegen braucht
                //er immer den Master Server
                announceMasterServerToAllClients();
            }

        }
    }

    private synchronized void StartNewGame()
    {
        if (_game == null)
        {
            _game = new Game("Alcatraz The Game");
        }
        else
        {
            synchronized (_game.GetSyncObject())
            {
                _game.GetClients().clear();     //Liste von Spielern leeren--> neues Spiel
            }
        }
    }

    /**
     * Lokale isMaster abfrage, damit wir keinen RMI call machen müssen.
     * @return true, wenn der lokale server der Master ist ansosnten false
     */
    private Boolean isMaster()
    {
        return _isMaster;
    }

    /*
     * Mit dieser Methode wird das SPiel auf den Clients gestartet.
     * */
    private synchronized void startGameOnClients(RegisterType type)
    {
        if (_game != null)
        {
            synchronized (_game.GetSyncObject())
            {
                Boolean allClientsStillAlive = true;
                Vector<IClient> removeList = new Vector<IClient>();
                for (IClient cl : _game.GetClients())   //Vor dem wirklichen aussenden, dass das Spiel starten soll, noch eine Sicherheitsüberprüfung
                {
                    boolean sucessfullySend = false;
                    int sendCounter = 0;
                    // timeout loop
                    while (sucessfullySend == false)
                    {
                        try
                        {
                            // try to reach other client
                            cl.IsAlive();
                            sucessfullySend = true;
                        }
                        catch (RemoteException ex)
                        {
                            if (!waitForTimeOut(ex, ++sendCounter))
                            {
                                allClientsStillAlive = false;
                                removeList.add(cl); //Der RemoveListe hinzufügen, um diese Clients nach der For-Schleife zu entfernen
                                Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
                                break;  //While schleife verlassen
                            }
                        }
                    }
                }
                for (IClient cl : removeList)
                {
                    _game.GetClients().remove(cl);      //Aus der Liste nehmen, ganz unten werden die Server wieder gesynct
                }

                if (allClientsStillAlive && CheckStartGameCondition(type))   //Nur wenn noch alle Clients da sind und die StartBedingungen noch immer erfüllt sind
                {
                    for (IClient cl : _game.GetClients())
                    {
                         boolean sucessfullySend = false;
                        int sendCounter = 0;
                        // timeout loop
                        while (sucessfullySend == false)
                        {
                            try
                            {
                                // try to reach other client
                                cl.DoStartGame(_game.GetClients(), _game.GetClients().firstElement());
                                sucessfullySend = true;
                            }
                            catch (RemoteException ex)
                            {
                                if (!waitForTimeOut(ex, ++sendCounter))
                                {
                                    allClientsStillAlive = false;
                                    removeList.add(cl); //Der RemoveListe hinzufügen, um diese Clients nach der For-Schleife zu entfernen
                                    Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
                                    break;  //While schleife verlassen
                                }
                            }
                        }
                    }
                    if(allClientsStillAlive)   //Nur durchführen wenn alles gepasst hat bei den Clients
                        StartNewGame();   //Game zurücksetzen, damit es beim nächsten Register neu anfängt.
                }

                if(allClientsStillAlive)    //Nur durchführen wenn alles gepasst hat bei den Clients
                forceSync(_game.GetClients());  //Den anderen Servern das mitteilen, entweder neues Spiel oder ein Client lebt nicht mehr
            }
        }
        //else -->dann stimmt so wie so etwas nicht
    }

    /**
     * Diese Methode kümmert sich um den TimeoutMechanismus!
     */
    private Boolean waitForTimeOut(RemoteException ex, int sendCounter)
    {
        Logger.getLogger(GameServerImpl.class.getName()).log(Level.WARNING, "Try to reach client...", "");
        if (sendCounter == IGameServer.MAX_SENDTRIES)
        {
            Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, "Unable to reach client... Giving up...", ex);
            return false;  //Wenn die Max Versuche e
        }
        try
        {
            Thread.sleep(IGameServer.WAIT_PERIOD);
        }
        catch (InterruptedException ex1)
        {
            // terminating Client;--> keine Ahnung wann so etwas kommt, wir haben damit auf jeden Fall nichts zu tun wir unterbrechen niemals einen Thread...
            Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, "Serious System Problem. Could not pause Thread. Terminating...", ex1);
            System.exit(1);
        }
        return true;
    }

    /*
     * Überprüft abhängig vom Typ ob das Spiel gestartet werden darf oder nicht.
     * @return true wenn das Spiel gestartet werden darf, ansonsten false
     * */
    private Boolean CheckStartGameCondition(RegisterType type)
    {
        if (_game != null)
        {
            synchronized (_game.GetSyncObject())
            {
                switch (type)
                {
                    case ForceStart:
                    {
                        if (_game.GetClients().size() >= IGameServer.MinClientsOnGame)
                        {
                            return true;
                        }
                        else
                        {
                            return false;
                        }
                    }
                    case NormalRegister:
                    {
                        if (_game.GetClients().size() == IGameServer.MaxClientsOnGame)
                        {
                            return true;
                        }
                        else
                        {
                            return false;
                        }
                    }
                    default:
                        return false;

                }
            }
        }
        else
        {
            return false;
        }
    }

    /**
     * Mit dieser Methode werden die Clients über MasterServer Änderungen benachrichtigt. Ist vor allem wichtig zwischen der Zeit der Registrierung
     * und dem tatsächlichen Start des Spiels.
     */
    private void announceMasterServerToAllClients()
    {
        if (_game != null)
        {
            synchronized (_game.GetSyncObject())
            {
                for (IClient client : _game.GetClients())
                {
                    try
                    {
                        client.AnnounceMasterServer(this._currentMaster);
                    }
                    catch (RemoteException ex)
                    {
                        Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
        }
        //else -->dann stimmt so wie so etwas nicht
    }

    /**
     * Prüft ob der SpielerNamen in diesem Spiel bereits vergeben ist.
     * @param playerName - Der Spielername welcher neu dazukommen möchte.
     * @return true wenn bereits vergeben, ansonsten false
     */
    private Boolean isPlayerNameAlreadyAssigned(String playerName)
    {
        if (_game != null)
        {
            synchronized (_game.GetSyncObject())
            {
                for (IClient client : _game.GetClients())
                {
                    try
                    {
                        if (client.GetPlayerName().equals(playerName))
                        {
                            return true;
                        }
                    }
                    catch (RemoteException ex)
                    {
                        Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
        }
        return false;
    }

    /*
     * Wird Remote aufgerufen von den Clients, um das SPiel sofort zu starten.
     * */
    public void startGameNow(IClient client) throws RemoteException, ImNotMasterServerException, PlayernameAlreadyAssigned
    {
        if (isMaster())
        {
            registerClient(client);

            if (CheckStartGameCondition(RegisterType.ForceStart))
            {
                startGameOnClients(RegisterType.ForceStart);
            }
        }
        else
        {
            throw new ImNotMasterServerException((IGameServer) this._currentMaster);
        }
    }

    /*
     * Wird remote Aufgerufen, um sich für das Spiel zu registrieren
     * */
    public void registerClient(IClient client) throws RemoteException, ImNotMasterServerException, PlayernameAlreadyAssigned
    {
        if (isMaster())
        {
            if (_game != null)
            {
                String playerName = client.GetPlayerName();
                if (isPlayerNameAlreadyAssigned(playerName))
                {
                    throw new PlayernameAlreadyAssigned(playerName);
                }
                synchronized (_game.GetSyncObject())
                {
                    if (!_game.GetClients().contains(client))        //Sicherheitsüberprüfung
                    {
                        _game.GetClients().add(client);
                        forceSync(_game.GetClients());
                    }


                    if (CheckStartGameCondition(RegisterType.NormalRegister))
                    {
                        startGameOnClients(RegisterType.NormalRegister);
                    }
                }
            }
            //else -->dann stimmt so wie so etwas nicht
        }
        else
        {
            throw new ImNotMasterServerException((IGameServer) this._currentMaster);
        }
    }

    /*
     * Wird remote Aufgerufen, um sich für das Spiel zu de-registrieren
     * */
    public void unregisterClient(IClient client) throws RemoteException, ImNotMasterServerException
    {
        if (isMaster())
        {
            if (_game != null)
            {
                synchronized (_game.GetSyncObject())
                {
                    if (_game.GetClients().contains(client))        //Sicherheitsüberprüfung
                    {
                        _game.GetClients().remove(client);
                        forceSync(_game.GetClients());
                    }
                }
            }
            //else: wenn es kein Spiel gibt, brauchen wir auch niemanden aus der Spielerlsite entfernen
        }
        else
        {
            throw new ImNotMasterServerException((IGameServer) this._currentMaster);
        }
    }


    /*
     * SPREAD - AdvancedMessageListener
     * Wir verwenden den AdvancedMessageListener da es für unsere Zwecke vollkommen ausreicht.
     * Ein Problem wäre es, wenn man nach Verlust der Netzwerkverbindung eines Servers, diesen wieder Reconnecten lässt,
     * weil dass würde beim Listener nicht richtig funktionieren, weil er uns sofort eine Leave-Message schicken würde
     * und dann müssen wir davon ausgehen, dass dieser Server weg ist. Danach könnte er sich wieder komplett neu reconnecten.
     *
     * Laut Herrn Froihofer ist des wurscht! Das wird nicht benotet, es muss nur sichergestellt sein, dass der Client einige Zeit vom Netz genommen
     * werden kann und nach dem reconnect, muss wieder alles gehen!
     * */
    /**
     * Method to receive a Message from anyone in the SPREAD Group
     * @param arg0
     */
    public void regularMessageReceived(SpreadMessage arg0)
    {
        MembershipInfo mi = arg0.getMembershipInfo();
        MessageType mType = MessageType.Parse(arg0.getType());

        switch (mType)
        {
            case ElectionMaster:
            {
                Object obj = null;
                try
                {
                    obj = arg0.getObject();
                }
                catch (SpreadException ex)
                {
                    Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
                }
                SwitchToMasterMode(obj);    //Lokales umswitchen in den MasterMode
                break;
            }
            case ClientSync:
            {
                if (_game != null)
                {
                    try
                    {
                        Vector<IClient> list = null;
                        list = (Vector<IClient>) arg0.getObject();

                        synchronized (_game.GetSyncObject())
                        {
                            _game.GetClients().clear(); //Vor dem Update lite zurücksetzen, dann braucht man nur mit Add arbeiten-->einfacher
                            for (IClient client : list)
                            {
                                //if(!_game.GetClients().contains(client))
                                _game.GetClients().add(client);
                            }
                        }
                    }
                    catch (SpreadException ex)
                    {
                        Logger.getLogger(GameServerImpl.class.getName()).log(Level.SEVERE, null, ex);
                    }

                }
                //TODO: Checken der Clients und die Spielerliste für das aktuelle Spiel updaten
                break;
            }
//            case ServerSync:
//            {
//                break;
//            }
            default:
                break;
        }
    }

    /**
     * Method to receive Membership Changes(for example:New Member in Group or Member leaves)
     * @param arg0
     */
    public void membershipMessageReceived(SpreadMessage arg0)
    {
        MembershipInfo mi = arg0.getMembershipInfo();
        SpreadGroup members[] = mi.getMembers();

        if (mi.isRegularMembership())
        {
            if (mi.isCausedByJoin())
            {
                if (members.length == 1)     //Ich bin der Erste, also werde ich zum Master
                {
                    SwitchToMasterMode(this._skeleton);   //Lokales umswitchen in den MasterMode
                }
                //Keine MasterSelection, weil ja niemand weggekommen ist, somit muss der Master auch noch da sein
                System.out.println("Es ist jemand neuer dazugekommen");
                if (this._isMaster && _game != null)          //Damit nur der Master die Daten verteilt
                {
                    synchronized (_game.GetSyncObject())
                    {
                        this.forceSync(_game.GetClients());
                    }
                }
            }
            else
            {
                if (mi.isCausedByLeave() || mi.isCausedByDisconnect() || mi.isCausedByNetwork())
                {
                    System.out.println("Jemand hat die Gruppe verlassen / Ein Netzwerkproblem ist aufgetreten");
                    System.out.println(members[0].toString());
                    //Eigentlich müsste ich jetzt alle Server durchfragen ob sie Server sind, weil es kann sich ein Server
                    //vorne eingeschoben haben.
                    //Remarks: Muss man nicht mehr machen, weil wir immer eine Election machen, wenn nicht der member[0] der Master ist
                    if (members[0].toString().contains(this._instanceName))
                    {
                        System.out.println("Ich bin ArrayElement 0 - Master");
                        if (!this.isMaster())
                        {     //Da ich lokal abfrage, mache ich keinen RMI call

                            System.out.println("Ich werde neuer Master");
                            SwitchToMasterMode(this._skeleton);    //Lokales umswitchen in den MasterMode
                            this.electionOfNewMaster();
                        }
                    }
                }
            }
        }
    }
}
